{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "!pip install langchain_openai langchain_core langgraph "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "import uuid\n",
    "from typing import Annotated\n",
    "# from langchain_core.messages import AIMessage\n",
    "from langchain_openai import ChatOpenAI\n",
    "from langchain_core.runnables import RunnableConfig\n",
    "from langgraph.graph import StateGraph, MessagesState, START, END\n",
    "from langgraph.store.base import BaseStore\n",
    "from typing import Annotated, Optional\n",
    "from langchain_core.tools import InjectedToolArg, tool\n",
    "\n",
    "from langgraph.store.memory import InMemoryStore\n",
    "from langgraph.checkpoint.memory import MemorySaver\n",
    "in_memory_store = InMemoryStore()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "\n",
    "@tool\n",
    "def upsert_memory(\n",
    "    content: str,\n",
    "    context: str,\n",
    "    memory_id: Optional[str] = None,\n",
    "    *,\n",
    "    config: Annotated[RunnableConfig, InjectedToolArg],\n",
    "    store: Annotated[BaseStore, InjectedToolArg],\n",
    "):\n",
    "    \"\"\"Upsert a memory in the database.\n",
    "\n",
    "    If a memory conflicts with an existing one, then just UPDATE the\n",
    "    existing one by passing in memory_id - don't create two memories\n",
    "    that are the same. If the user corrects a memory, UPDATE it.\n",
    "\n",
    "    Args:\n",
    "        content: The main content of the memory. For example:\n",
    "            \"User expressed interest in learning about French.\"\n",
    "        context: Additional context for the memory. For example:\n",
    "            \"This was mentioned while discussing career options in Europe.\"\n",
    "        memory_id: ONLY PROVIDE IF UPDATING AN EXISTING MEMORY.\n",
    "        The memory to overwrite.\n",
    "    \"\"\"\n",
    "    mem_id = memory_id or uuid.uuid4()\n",
    "    user_id = config[\"configurable\"][\"user_id\"]\n",
    "    store.put(\n",
    "        (\"memories\", user_id),\n",
    "        key=str(mem_id),\n",
    "        value={\"content\": content, \"context\": context},\n",
    "    )\n",
    "    return f\"Stored memory {mem_id}\""
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [],
   "source": [
    "model = ChatOpenAI(model=\"gpt-4o\")\n",
    "\n",
    "def store_memory(state: MessagesState, config: RunnableConfig, store: BaseStore):\n",
    "    # Extract tool calls from the last message\n",
    "    tool_calls = state[\"messages\"][-1].tool_calls\n",
    "    saved_memories=[]\n",
    "    for tc in tool_calls:\n",
    "        content = tc['args']['content']\n",
    "        context = tc['args']['context']\n",
    "        saved_memories.append([\n",
    "            upsert_memory.invoke({'content': content, 'context': context, 'config':config, 'store':store})  \n",
    "        ])\n",
    "    # print(\"saved_memories: \", saved_memories)\n",
    "    # Format the results of memory storage operations\n",
    "    # This provides confirmation to the model that the actions it took were completed\n",
    "    results = [\n",
    "        {\n",
    "            \"role\": \"tool\",\n",
    "            \"content\": mem[0],\n",
    "            \"tool_call_id\": tc[\"id\"],\n",
    "        }\n",
    "        for tc, mem in zip(tool_calls, saved_memories)\n",
    "    ]\n",
    "    # print(results)\n",
    "    return {\"messages\": results[0]}\n",
    "\n",
    "def route_message(state: MessagesState):\n",
    "    \"\"\"Determine the next step based on the presence of tool calls.\"\"\"\n",
    "    msg = state[\"messages\"][-1]\n",
    "    if msg.tool_calls:\n",
    "        # If there are tool calls, we need to store memories\n",
    "        return \"store_memory\"\n",
    "    # Otherwise, finish; user can send the next message\n",
    "    return END\n",
    "\n",
    "def call_model(state: MessagesState, config: RunnableConfig, *, store: BaseStore):\n",
    "    user_id = config[\"configurable\"][\"user_id\"]\n",
    "    namespace = (\"memories\", user_id)\n",
    "    memories = store.search(namespace)\n",
    "    info = \"\\n\".join(f\"[{mem.key}]: {mem.value}\" for mem in memories)\n",
    "    if info:\n",
    "        info = f\"\"\"\n",
    "    <memories>\n",
    "    {info}\n",
    "    </memories>\"\"\"\n",
    "    \n",
    "    system_msg = f'''You are a helpful assistant talking to the user. You must decide whether to store information as memory from list of messages and then answer the user query or directly answer the user query\n",
    "        User context info: {info}'''\n",
    "    print(\"system_msg:\", system_msg)\n",
    "    # Store new memories if the user asks the model to remember\n",
    "    last_message = state[\"messages\"][-1]\n",
    "    # if \"remember\" in last_message.content.lower():\n",
    "    #     memory = \"User name is Bob\"\n",
    "    #     store.put(namespace, str(uuid.uuid4()), {\"data\": memory})\n",
    "    # print( [{\"type\": \"system\", \"content\": system_msg}] + state[\"messages\"])\n",
    "    response = model.bind_tools([upsert_memory]).invoke(\n",
    "        [{\"type\": \"system\", \"content\": system_msg}] + state[\"messages\"]\n",
    "    )\n",
    "    return {\"messages\": response}\n",
    "\n",
    "\n",
    "builder = StateGraph(MessagesState)\n",
    "builder.add_node(\"call_model\", call_model)\n",
    "builder.add_edge(START, \"call_model\")\n",
    "\n",
    "builder.add_node(store_memory)\n",
    "\n",
    "builder.add_conditional_edges(\"call_model\", route_message, [\"store_memory\", END])\n",
    "\n",
    "builder.add_edge(\"store_memory\", \"call_model\")\n",
    "\n",
    "graph = builder.compile(store=in_memory_store)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "================================\u001b[1m Human Message \u001b[0m=================================\n",
      "\n",
      "Hi! Remember: my name is Bob\n",
      "system_msg: You are a helpful assistant talking to the user. You must decide whether to store information as memory from list of messages and then answer the user query or directly answer the user query\n",
      "        User context info: \n",
      "[{'type': 'system', 'content': 'You are a helpful assistant talking to the user. You must decide whether to store information as memory from list of messages and then answer the user query or directly answer the user query\\n        User context info: '}, HumanMessage(content='Hi! Remember: my name is Bob', additional_kwargs={}, response_metadata={}, id='7ce923c3-1b4c-4487-891f-40273067f60b')]\n",
      "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
      "Tool Calls:\n",
      "  upsert_memory (call_PrjfPMYG7sYUlJJE6gdMQSWQ)\n",
      " Call ID: call_PrjfPMYG7sYUlJJE6gdMQSWQ\n",
      "  Args:\n",
      "    content: User's name is Bob.\n",
      "    context: User mentioned their name.\n",
      "saved_memories:  [['Stored memory e424aeb4-7cb0-4c1f-90ed-efb7da4333c7']]\n",
      "[{'role': 'tool', 'content': 'Stored memory e424aeb4-7cb0-4c1f-90ed-efb7da4333c7', 'tool_call_id': 'call_PrjfPMYG7sYUlJJE6gdMQSWQ'}]\n",
      "=================================\u001b[1m Tool Message \u001b[0m=================================\n",
      "\n",
      "Stored memory e424aeb4-7cb0-4c1f-90ed-efb7da4333c7\n",
      "system_msg: You are a helpful assistant talking to the user. You must decide whether to store information as memory from list of messages and then answer the user query or directly answer the user query\n",
      "        User context info: \n",
      "    <memories>\n",
      "    [e424aeb4-7cb0-4c1f-90ed-efb7da4333c7]: {'content': \"User's name is Bob.\", 'context': 'User mentioned their name.'}\n",
      "    </memories>\n",
      "[{'type': 'system', 'content': 'You are a helpful assistant talking to the user. You must decide whether to store information as memory from list of messages and then answer the user query or directly answer the user query\\n        User context info: \\n    <memories>\\n    [e424aeb4-7cb0-4c1f-90ed-efb7da4333c7]: {\\'content\\': \"User\\'s name is Bob.\", \\'context\\': \\'User mentioned their name.\\'}\\n    </memories>'}, HumanMessage(content='Hi! Remember: my name is Bob', additional_kwargs={}, response_metadata={}, id='7ce923c3-1b4c-4487-891f-40273067f60b'), AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_PrjfPMYG7sYUlJJE6gdMQSWQ', 'function': {'arguments': '{\"content\":\"User\\'s name is Bob.\",\"context\":\"User mentioned their name.\"}', 'name': 'upsert_memory'}, 'type': 'function'}], 'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 27, 'prompt_tokens': 225, 'total_tokens': 252, 'completion_tokens_details': {'audio_tokens': None, 'reasoning_tokens': 0}, 'prompt_tokens_details': {'audio_tokens': None, 'cached_tokens': 0}}, 'model_name': 'gpt-4o-2024-08-06', 'system_fingerprint': 'fp_45c6de4934', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-a6f0ba23-16b2-4611-ba8f-01aada6a556c-0', tool_calls=[{'name': 'upsert_memory', 'args': {'content': \"User's name is Bob.\", 'context': 'User mentioned their name.'}, 'id': 'call_PrjfPMYG7sYUlJJE6gdMQSWQ', 'type': 'tool_call'}], usage_metadata={'input_tokens': 225, 'output_tokens': 27, 'total_tokens': 252}), ToolMessage(content='Stored memory e424aeb4-7cb0-4c1f-90ed-efb7da4333c7', id='6dab31c8-1993-4640-8bab-ebe025acc1a7', tool_call_id='call_PrjfPMYG7sYUlJJE6gdMQSWQ')]\n",
      "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
      "\n",
      "Hi Bob! I've got your name remembered. How can I assist you today?\n"
     ]
    }
   ],
   "source": [
    "config = {\"configurable\": {\"thread_id\": \"1\", \"user_id\": \"1\"}}\n",
    "input_message = {\"type\": \"user\", \"content\": \"Hi! My name is Bob. I love keep updated on Latest Tech\"}\n",
    "for chunk in graph.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n",
    "    chunk[\"messages\"][-1].pretty_print()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "================================\u001b[1m Human Message \u001b[0m=================================\n",
      "\n",
      "What do you know about me\n",
      "system_msg: You are a helpful assistant talking to the user. You must decide whether to store information as memory from list of messages and then answer the user query or directly answer the user query\n",
      "        User context info: \n",
      "    <memories>\n",
      "    [e424aeb4-7cb0-4c1f-90ed-efb7da4333c7]: {'content': \"User's name is Bob.\", 'context': 'User mentioned their name.'}\n",
      "    </memories>\n",
      "[{'type': 'system', 'content': 'You are a helpful assistant talking to the user. You must decide whether to store information as memory from list of messages and then answer the user query or directly answer the user query\\n        User context info: \\n    <memories>\\n    [e424aeb4-7cb0-4c1f-90ed-efb7da4333c7]: {\\'content\\': \"User\\'s name is Bob.\", \\'context\\': \\'User mentioned their name.\\'}\\n    </memories>'}, HumanMessage(content='What do you know about me', additional_kwargs={}, response_metadata={}, id='21fe0c1b-2ea9-427f-ae18-3c8ef8b40c31')]\n",
      "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
      "\n",
      "You mentioned that your name is Bob. Is there anything else you'd like to share or update?\n"
     ]
    }
   ],
   "source": [
    "config = {\"configurable\": {\"thread_id\": \"1\", \"user_id\": \"1\"}}\n",
    "input_message = {\"type\": \"user\", \"content\": \"What do you know about me\"}\n",
    "for chunk in graph.stream({\"messages\": [input_message]}, config, stream_mode=\"values\"):\n",
    "    chunk[\"messages\"][-1].pretty_print()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[e424aeb4-7cb0-4c1f-90ed-efb7da4333c7]: {'content': \"User's name is Bob.\", 'context': 'User mentioned their name.'}\n"
     ]
    }
   ],
   "source": [
    "namespace = (\"memories\", \"1\")\n",
    "memories = in_memory_store.search(namespace)\n",
    "info = \"\\n\".join(f\"[{mem.key}]: {mem.value}\" for mem in memories)\n",
    "print(info)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/jpeg": "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",
      "text/plain": [
       "<IPython.core.display.Image object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "from IPython.display import Image, display\n",
    "try:\n",
    "    display(Image(graph.get_graph().draw_mermaid_png()))\n",
    "except Exception as e:\n",
    "    print(e)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "futuresmart",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.4"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
